<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover"><title>wolf2w项目经验_站内搜索与接口安全 | LenlBlog</title><meta name="author" content="Lenl,lenl412@163.com"><meta name="copyright" content="Lenl"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="ffffff"><meta name="description" content="一个SpringBoot+Vue的项目教程，还包含了MongoDB、ElasticSearch等技术的基本使用。本文为站内搜索与接口安全的应用经验部分。">
<meta property="og:type" content="article">
<meta property="og:title" content="wolf2w项目经验_站内搜索与接口安全">
<meta property="og:url" content="https://lienle.gitee.io/notes/wolf2w_2/index.html">
<meta property="og:site_name" content="LenlBlog">
<meta property="og:description" content="一个SpringBoot+Vue的项目教程，还包含了MongoDB、ElasticSearch等技术的基本使用。本文为站内搜索与接口安全的应用经验部分。">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png">
<meta property="article:published_time" content="2023-05-01T01:19:19.000Z">
<meta property="article:modified_time" content="2023-05-02T07:33:23.362Z">
<meta property="article:author" content="Lenl">
<meta property="article:tag" content="SpringBoot">
<meta property="article:tag" content="ElasticSearch">
<meta property="article:tag" content="接口安全">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png"><link rel="shortcut icon" href="/img/lenl.png"><link rel="canonical" href="https://lienle.gitee.io/notes/wolf2w_2/index.html"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//hm.baidu.com"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><meta name="baidu-site-verification" content="codeva-6HME93uF43"/><link rel="stylesheet" href="/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox/fancybox.min.css" media="print" onload="this.media='all'"><script>var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://hm.baidu.com/hm.js?629608ec0c773bd9f96cb72389693ab9";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
</script><script>const GLOBAL_CONFIG = { 
  root: '/',
  algolia: undefined,
  localSearch: {"path":"/search.xml","preload":false,"top_n_per_article":1,"unescape":false,"languages":{"hits_empty":"找不到您查询的内容：${query}","hits_stats":"共找到 ${hits} 篇文章"}},
  translate: undefined,
  noticeOutdate: undefined,
  highlight: {"plugin":"highlighjs","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":false},
  copy: {
    success: '复制成功',
    error: '复制错误',
    noSupport: '浏览器不支持'
  },
  relativeDate: {
    homepage: false,
    post: false
  },
  runtime: '',
  dateSuffix: {
    just: '刚刚',
    min: '分钟前',
    hour: '小时前',
    day: '天前',
    month: '个月前'
  },
  copyright: {"limitCount":50,"languages":{"author":"作者: Lenl","link":"链接: ","source":"来源: LenlBlog","info":"著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。"}},
  lightbox: 'fancybox',
  Snackbar: undefined,
  source: {
    justifiedGallery: {
      js: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.js',
      css: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.css'
    }
  },
  isPhotoFigcaption: true,
  islazyload: true,
  isAnchor: false,
  percent: {
    toc: true,
    rightside: false,
  }
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
  title: 'wolf2w项目经验_站内搜索与接口安全',
  isPost: true,
  isHome: false,
  isHighlightShrink: false,
  isToc: true,
  postUpdate: '2023-05-02 15:33:23'
}</script><noscript><style type="text/css">
  #nav {
    opacity: 1
  }
  .justified-gallery img {
    opacity: 1
  }

  #recent-posts time,
  #post-meta time {
    display: inline !important
  }
</style></noscript><script>(win=>{
    win.saveToLocal = {
      set: function setWithExpiry(key, value, ttl) {
        if (ttl === 0) return
        const now = new Date()
        const expiryDay = ttl * 86400000
        const item = {
          value: value,
          expiry: now.getTime() + expiryDay,
        }
        localStorage.setItem(key, JSON.stringify(item))
      },

      get: function getWithExpiry(key) {
        const itemStr = localStorage.getItem(key)

        if (!itemStr) {
          return undefined
        }
        const item = JSON.parse(itemStr)
        const now = new Date()

        if (now.getTime() > item.expiry) {
          localStorage.removeItem(key)
          return undefined
        }
        return item.value
      }
    }
  
    win.getScript = url => new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = url
      script.async = true
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    })
  
    win.getCSS = (url,id = false) => new Promise((resolve, reject) => {
      const link = document.createElement('link')
      link.rel = 'stylesheet'
      link.href = url
      if (id) link.id = id
      link.onerror = reject
      link.onload = link.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        link.onload = link.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(link)
    })
  
      win.activateDarkMode = function () {
        document.documentElement.setAttribute('data-theme', 'dark')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
        }
      }
      win.activateLightMode = function () {
        document.documentElement.setAttribute('data-theme', 'light')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', 'ffffff')
        }
      }
      const t = saveToLocal.get('theme')
    
          if (t === 'dark') activateDarkMode()
          else if (t === 'light') activateLightMode()
        
      const asideStatus = saveToLocal.get('aside-status')
      if (asideStatus !== undefined) {
        if (asideStatus === 'hide') {
          document.documentElement.classList.add('hide-aside')
        } else {
          document.documentElement.classList.remove('hide-aside')
        }
      }
    
    const detectApple = () => {
      if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
        document.documentElement.classList.add('apple')
      }
    }
    detectApple()
    })(window)</script></head><body><!--include ./fullpage-loading.pug--><div id="loading-box"><div class="loading-left-bg"></div><div class="loading-right-bg"></div><div class="wizard-scene"><div class="wizard-objects"><div class="wizard-square"></div><div class="wizard-circle"></div><div class="wizard-triangle"></div></div><div class="wizard"><div class="wizard-body"></div><div class="wizard-right-arm"><div class="wizard-right-hand"></div></div><div class="wizard-left-arm"><div class="wizard-left-hand"></div></div><div class="wizard-head"><div class="wizard-beard"></div><div class="wizard-face"><div class="wizard-adds"></div></div><div class="wizard-hat"><div class="wizard-hat-of-the-hat"></div><div class="wizard-four-point-star --first"></div><div class="wizard-four-point-star --second"></div><div class="wizard-four-point-star --third"></div></div></div></div></div></div><script>const preloader = {
  endLoading: () => {
    document.body.style.overflow = '';
    document.getElementById('loading-box').classList.add("loaded")
  },
  initLoading: () => {
    document.body.style.overflow = 'hidden';
    document.getElementById('loading-box').classList.remove("loaded")
  }
}

preloader.initLoading()
window.addEventListener('load',()=> { preloader.endLoading() })
//避免加载时间过长
setTimeout(function(){preloader.endLoading();}, 3000);

if (false) {
  document.addEventListener('pjax:send', () => { preloader.initLoading() })
  document.addEventListener('pjax:complete', () => { preloader.endLoading() })
}</script><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src= "/img/hexo_loading.gif" data-lazy-src="/img/lenl.png" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="sidebar-site-data site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">17</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">25</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">8</div></a></div><hr/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 主页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 存档</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 分享</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/collect/"><i class="fa-fw fas fa-book"></i><span> 大佬们の干货</span></a></li><li><a class="site-page child" href="/resources/"><i class="fa-fw fab fa-dropbox"></i><span> 屯屯鼠の资源</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-link"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div></div></div><div class="post" id="body-wrap"><header class="post-bg fixed" id="page-header" style="background-image: url('https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png')"><nav id="nav"><span id="blog-info"><a href="/" title="LenlBlog"><img class="site-icon" src= "/img/hexo_loading.gif" data-lazy-src="/img/lenl.png"/><span class="site-name">LenlBlog</span></a></span><div id="menus"><div id="search-button"><a class="site-page social-icon search" href="javascript:void(0);"><i class="fas fa-search fa-fw"></i><span> 搜索</span></a></div><div class="menus_items"><div class="menus_item"><a class="site-page" href="/"><i class="fa-fw fas fa-home"></i><span> 主页</span></a></div><div class="menus_item"><a class="site-page" href="/archives/"><i class="fa-fw fas fa-archive"></i><span> 存档</span></a></div><div class="menus_item"><a class="site-page" href="/tags/"><i class="fa-fw fas fa-tags"></i><span> 标签</span></a></div><div class="menus_item"><a class="site-page" href="/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分类</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fas fa-list"></i><span> 分享</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/collect/"><i class="fa-fw fas fa-book"></i><span> 大佬们の干货</span></a></li><li><a class="site-page child" href="/resources/"><i class="fa-fw fab fa-dropbox"></i><span> 屯屯鼠の资源</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/link/"><i class="fa-fw fas fa-link"></i><span> 友链</span></a></div><div class="menus_item"><a class="site-page" href="/about/"><i class="fa-fw fas fa-heart"></i><span> 关于</span></a></div></div><div id="toggle-menu"><a class="site-page" href="javascript:void(0);"><i class="fas fa-bars fa-fw"></i></a></div></div></nav><div id="post-info"><h1 class="post-title">wolf2w项目经验_站内搜索与接口安全</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">发表于</span><time class="post-meta-date-created" datetime="2023-05-01T01:19:19.000Z" title="发表于 2023-05-01 09:19:19">2023-05-01</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">更新于</span><time class="post-meta-date-updated" datetime="2023-05-02T07:33:23.362Z" title="更新于 2023-05-02 15:33:23">2023-05-02</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/categories/%E7%BC%96%E7%A8%8B%E9%9A%8F%E8%AE%B0/">编程随记</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-wordcount"><i class="far fa-file-word fa-fw post-meta-icon"></i><span class="post-meta-label">字数总计:</span><span class="word-count">6.9k</span><span class="post-meta-separator">|</span><i class="far fa-clock fa-fw post-meta-icon"></i><span class="post-meta-label">阅读时长:</span><span>29分钟</span></span><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title="wolf2w项目经验_站内搜索与接口安全"><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">阅读量:</span><span id="busuanzi_value_page_pv"><i class="fa-solid fa-spinner fa-spin"></i></span></span><span class="post-meta-separator">|</span><span class="post-meta-commentcount"><i class="far fa-comments fa-fw post-meta-icon"></i><span class="post-meta-label">评论数:</span><a href="/notes/wolf2w_2/#post-comment" itemprop="discussionUrl"><span class="valine-comment-count" data-xid="/notes/wolf2w_2/" itemprop="commentCount"><i class="fa-solid fa-spinner fa-spin"></i></span></a></span></div></div></div></header><main class="layout" id="content-inner"><div id="post"><article class="post-content" id="article-container"><h1>wolf2w项目学习随记_2</h1>
<div class="note warning simple"><p>本随记为跟随B站视频<a target="_blank" rel="noopener" href="https://www.bilibili.com/video/BV1re4y1e7rJ" title="旅游出行项目学习">2022版 Spring Boot+Vue前后端分离项目实战</a> 学习时随手记下的一些经验整理。以便在后续学习开发中遇到类似问题能够找到解答依据。</p>
</div>
<h2 id="使用ElasticSearch实现站内搜索">使用ElasticSearch实现站内搜索</h2>
<ul class="lvl-0">
<li class="lvl-2">
<p>官方网站：<a target="_blank" rel="noopener" href="https://www.elastic.co/cn/elasticsearch/">Elasticsearch：官方分布式搜索和分析引擎 | Elastic</a></p>
</li>
<li class="lvl-2">
<p>中文文档：<a target="_blank" rel="noopener" href="https://elasticsearch.bookhub.tech/">Elasticsearch 翻译说明 | Elasticsearch 中文文档 (bookhub.tech)</a></p>
</li>
</ul>
<div class="note warning simple"><p>为笔记方便，以下对 <code>Elasticsearch</code> 简称为 <code>es</code> 。</p>
</div>
<h3 id="1-数据初始化准备">1 数据初始化准备</h3>
<blockquote>
<p>具体初始化实现分析：</p>
<ol>
<li class="lvl-3">
<p>要初始化哪些数据？ --攻略-目的地-游记-用户；即需要进行搜索的数据</p>
</li>
<li class="lvl-3">
<p>怎么初始化数据？ --监听器-servlet/controller-&gt;因为避免每次启动都要初始化，选用controller。</p>
</li>
<li class="lvl-3">
<p>初始化的数据内容？-- 将mysql表中数据中参与条件搜索的列(条件列)和id主键，初始化到es中</p>
<p><em>es数据变动的维护成本很大，因此不建议将所有列初始化到es中</em></p>
</li>
<li class="lvl-3">
<p>后续搜索时，先搜索es得到满足条件的数据id，然后将数据id作为条件查询mysql。</p>
</li>
</ol>
</blockquote>
<h4 id="1-1-引入与配置">1.1 引入与配置</h4>
<p>引入es的maven依赖：</p>
<figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-elasticsearch<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>
<p>在yml中设置对应的配置：</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment">#elasticseatch配置</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">elasticsearch:</span></span><br><span class="line">    <span class="attr">uris:</span> <span class="string">http://localhost:9200</span></span><br></pre></td></tr></table></figure>
<h4 id="1-2-创建es需要的对应的domain对象">1.2 创建es需要的对应的domain对象</h4>
<p>因为es初始化的数据为 <code>mysql表中数据中参与条件搜索的列(条件列)和id主键</code> ，因此domain对象也是这些字段即可。一个示例的domain对象如下：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Setter</span></span><br><span class="line"><span class="meta">@Document(indexName = &quot;userinfo&quot;)</span> <span class="comment">//指定索引名称</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserInfoEs</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">    <span class="comment">//常量识别字段</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String INDEX_NAME=<span class="string">&quot;userinfo&quot;</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//@Field 每个文档的字段配置（类型、是否分词、是否存储、分词器）</span></span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="meta">@Field(type = FieldType.Long)</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="meta">@Field(type = FieldType.Keyword)</span></span><br><span class="line">    <span class="keyword">private</span> String city;</span><br><span class="line">    <span class="meta">@Field(analyzer = &quot;ik_max_word&quot;,searchAnalyzer = &quot;ik_max_word&quot;,type = FieldType.Text)</span></span><br><span class="line">    <span class="keyword">private</span> String info;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>对于User信息，需要使用info和city字段进行检索，加上id主键，一共三个字段。</p>
<h4 id="1-3-生成对应的Repository">1.3 生成对应的Repository</h4>
<p>这一步一般继承 <code>ElasticsearchRepository</code> 即可，其中泛型第一个字段为domain对象，第二个为搜索类型。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserInfoEsRepository</span> <span class="keyword">extends</span> <span class="title class_">ElasticsearchRepository</span>&lt;UserInfoEs,String&gt; &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h4 id="1-4-生成对应的service和impl">1.4 生成对应的service和impl</h4>
<p>即类似于mysql和mongo的CRUD相关操作：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">IUserInfoEsService</span> &#123;</span><br><span class="line">    <span class="comment">/** 添加</span></span><br><span class="line"><span class="comment">    * <span class="doctag">@param</span> userInfoEs</span></span><br><span class="line"><span class="comment">    * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">save</span><span class="params">(UserInfoEs userInfoEs)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 更新</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> userInfoEs</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">update</span><span class="params">(UserInfoEs userInfoEs)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 查单个</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    UserInfoEs <span class="title function_">get</span><span class="params">(String id)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 查多个</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    List&lt;UserInfoEs&gt; <span class="title function_">list</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 删除</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">delete</span><span class="params">(String id)</span>;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>对应的impl：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserInfoEsServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">IUserInfoEsService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserInfoEsRepository repository;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">save</span><span class="params">(UserInfoEs userInfoEsEs)</span> &#123;</span><br><span class="line">        <span class="comment">//userInfoEsEs.setId(null);</span></span><br><span class="line">        repository.save(userInfoEsEs);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">update</span><span class="params">(UserInfoEs userInfoEsEs)</span> &#123;</span><br><span class="line">        repository.save(userInfoEsEs);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> UserInfoEs <span class="title function_">get</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> repository.findById(id).orElse(<span class="literal">null</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> List&lt;UserInfoEs&gt; <span class="title function_">list</span><span class="params">()</span> &#123;</span><br><span class="line">        List&lt;UserInfoEs&gt; list = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">        Iterable&lt;UserInfoEs&gt; all = repository.findAll();</span><br><span class="line">        all.forEach(a -&gt; list.add(a));</span><br><span class="line">        <span class="keyword">return</span> list;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">delete</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        repository.deleteById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="2-安装es">2 安装es</h3>
<p>安装es遇到了一些坑，因此在这里记录一下。这里以当前的最新版本8.7.0为例。</p>
<p>详细安装入门可参考这篇 <a target="_blank" rel="noopener" href="https://www.cnblogs.com/coderxz/p/13268417.html">保姆级教程</a></p>
<h4 id="2-1-下载安装es">2.1 下载安装es</h4>
<p>前往 <a target="_blank" rel="noopener" href="https://www.elastic.co/cn/elasticsearch/">es官网</a> 下载7.17.6版本的压缩包，解压到本地后的目录如下：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">bin</span><br><span class="line">config</span><br><span class="line">jdk</span><br><span class="line">lib</span><br><span class="line">logs</span><br><span class="line">modules</span><br><span class="line">plugins</span><br></pre></td></tr></table></figure>
<p>打开bin目录，双击 <code>elasticsearch.bat</code> 初次启动，8以上版本的es可能默认开启了ssl，因此需要使用https以及用户密码进行访问。此时可以修改 <code>config/elasticsearch.yml</code> 中的配置项，关闭ssl：</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Enable security features</span></span><br><span class="line"><span class="attr">xpack.security.enabled:</span> <span class="literal">false</span></span><br><span class="line"><span class="comment"># Enable encryption for HTTP API client connections, such as Kibana, Logstash, and Agents</span></span><br><span class="line"><span class="attr">xpack.security.http.ssl:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">false</span></span><br><span class="line"><span class="comment"># Enable encryption and mutual authentication between cluster nodes</span></span><br><span class="line"><span class="attr">xpack.security.transport.ssl:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure>
<p>如果打开yml文件发现没有上述配置，则需要初次启动一次bat才会生成这些。另外，可以加上支持跨域的一些配置：</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment">#使ES支持跨域请求</span></span><br><span class="line"><span class="attr">http.cors.enabled:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">http.cors.allow-origin:</span> <span class="string">&quot;*&quot;</span></span><br><span class="line"><span class="attr">network.host:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span></span><br></pre></td></tr></table></figure>
<p>再次启动bat，访问<a target="_blank" rel="noopener" href="http://localhost:9200/">localhost:9200</a> 能够得到一个json数据，即代表安装成功。</p>
<div class="note warning simple"><p>es启动后会自动为添加了注解的domain创建索引，除非手动指定 <code>createIndex=false</code></p>
</div>
<p>启动后如果出现一些其他报错，参考如下信息：</p>
<p><strong>问题1：内存直接占满</strong></p>
<p>此时你打开任务管理器，发现一个问题，ES启动后，占据了沙盒的全部内存。我的本机是32G的，启动也是直接打满，导致系统卡爆。因此我们需要配置启动内存，配置文件conf/jvm.options。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">-Xms1g</span><br><span class="line">-Xmx1g</span><br></pre></td></tr></table></figure>
<p><strong>问题2：二次启动报错</strong></p>
<p>修改配置文件后，再次启动，这时不会再次打印第一次启动时的配置信息了。但是报错了 “org.elasticsearch.ElasticsearchException: not all primary shards of [.geoip_databases] index are active” 。这是因为启动时会去更新地图的一些数据库，这里直接禁掉即可，用到时再说，配置文件onf/elasticsearch.yml，增加配置:</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">ingest.geoip.downloader.enabled:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure>
<h4 id="2-2-安装ik分词插件">2.2 安装ik分词插件</h4>
<p>es的许多功能需要额外安装插件，分词插件是很重要的插件。</p>
<p>前往 <a target="_blank" rel="noopener" href="https://github.com/medcl/elasticsearch-analysis-ik/releases">ik下载地址</a> 下载zip压缩包，这里存在很多版本，ik版本与es的版本参照可查阅 <a target="_blank" rel="noopener" href="https://github.com/medcl/elasticsearch-analysis-ik/blob/master/README.md">版本参照</a> 。</p>
<p>将zip解压后，会发现里层结构为 <code>config</code> 文件夹、一些jar包和一些配置文件。</p>
<p>在es的plugins文件夹下新建 <code>ik</code> 目录，将所解压的内容拷贝进去。</p>
<div class="note warning simple"><p>这里要注意，拷贝的是一系列jar包那一层，而不是直接解压后的含版本号的那个文件夹。</p>
</div>
<p>再次启动后，即可安装该插件。</p>
<h4 id="2-3-安装可视化插件">2.3 安装可视化插件</h4>
<div class="note info simple"><p>ElasticSearch不同于Solr自带图形化界面，我们可以通过安装ElasticSearch的head插件，完成图形化界面的效果，完成索引数据的查看。</p>
</div>
<p>下载地址： <a target="_blank" rel="noopener" href="https://github.com/mobz/elasticsearch-head">head插件下载地址</a></p>
<p>根据文档，es5以上的head网站插件方式已不支持，需要 <code>Run as a standalone server</code> 。</p>
<p>elasticsearch-head插件是使用JavaScript开发的，依赖Node.js库，使用Grunt工具构建，所以要安装elasticsearch-head，还需要先安装Node.js和Grunt。</p>
<p><code>git clone</code> 源码后执行 <code>npm install</code> 安装依赖，安装完成后执行 <code>npm run start</code> 启动head插件，随后访问 <code>http://localhost:9100/</code> 即可。集群健康值如下：</p>
<ul class="lvl-0">
<li class="lvl-2">
<p>绿色：最健康的状态，代表所有的分片包括备份都可用</p>
</li>
<li class="lvl-2">
<p>黄色：基本的分片可用，但是备份不可用（也可能是没有备份）</p>
</li>
<li class="lvl-2">
<p>红色：部分的分片可用，表明分片有一部分损坏。此时执行查询部分数据仍然可以查到，遇到这种情况，还是赶快解决比较好</p>
</li>
<li class="lvl-2">
<p>灰色：未连接到elasticsearch服务</p>
</li>
</ul>
<p>当健康值为灰色时，可能是因为未配置跨域，需要添加2.2中所说的yml配置。</p>
<div class="note warning simple"><p><code>npm install</code> 后可能会遇到报错 <code>PhantomJS not found on PATH</code> 此时需要先执行 <code>npm install phantomjs@2.1.1 --ignore-scripts</code> 然后再install即可。</p>
</div>
<h3 id="3-数据维护">3 数据维护</h3>
<h4 id="3-1-首次初始化">3.1 首次初始化</h4>
<p>创建一个controller实现es的首次初始化，以后就不再进行初始化，而通过其他逻辑进行数据的增量更新。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Controller</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DataController</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserinfoService userinfoService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IUserInfoEsService userInfoEsService;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">//项目第一次启动执行，后面就不执行了</span></span><br><span class="line">    <span class="comment">//后续另外使用其他逻辑实现数据增量更新</span></span><br><span class="line">    <span class="meta">@RequestMapping(&quot;/dataInit&quot;)</span></span><br><span class="line">    <span class="meta">@ResponseBody</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">dataInit</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="comment">//mysql ==&gt; es</span></span><br><span class="line"></span><br><span class="line">        List&lt;UserInfo&gt; us = userinfoService.list();</span><br><span class="line">        <span class="keyword">for</span> (UserInfo u : us) &#123;</span><br><span class="line">            <span class="type">UserInfoEs</span> <span class="variable">userInfoEs</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UserInfoEs</span>();</span><br><span class="line">            BeanUtils.copyProperties(u,userInfoEs);</span><br><span class="line">            userInfoEsService.save(userInfoEs);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;ok&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>服务跑起来后直接访问该接口，将数据初始化到es中。通过可视化插件可以查看到详情。</p>
<h4 id="3-2-mysql与es数据同步">3.2 mysql与es数据同步</h4>
<blockquote>
<p>当mysql中数据发生改动时，es中是没法同步更新的，需要进行数据同步。</p>
</blockquote>
<p>可选方案：</p>
<ol>
<li class="lvl-3">
<p>在更动接口实现es同步更新–不建议，耗时+事务问题+单一责任原则</p>
</li>
<li class="lvl-3">
<p>使用异步方式进行es数据同步：使用spring的自定义事件。</p>
<ul class="lvl-2">
<li class="lvl-5">自定义一个mysql数据更新事件；</li>
<li class="lvl-5">一旦代码中发生了数据更新，马上触发该事件；</li>
<li class="lvl-5">定义一个spring监听器，监听该事件，一旦事件触发，马上执行es数据同步逻辑。</li>
</ul>
</li>
</ol>
<p>监听器在redis的数据初始化有涉及。自定义事件的实现方式可以参照文章：<a target="_blank" rel="noopener" href="https://blog.csdn.net/C_AJing/article/details/109803124">自定义事件发布与监听</a></p>
<ol start="3">
<li class="lvl-3">
<p>使用消息中间件-最佳方式。</p>
<ul class="lvl-2">
<li class="lvl-5">mysql更新成功之后，往消息队列中推送一条数据；</li>
<li class="lvl-5">消息队列接收后在es中进行数据同步更新。</li>
</ul>
</li>
<li class="lvl-3">
<p>使用第三方数据同步组件：canal</p>
</li>
</ol>
<h3 id="4-全文检索实现">4 全文检索实现</h3>
<p>为了实现全文索引并将检索词高亮显示，定义一个Service进行高亮查询：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">  * 全文搜索 + 高亮显示</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@param</span> index 索引</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@param</span> clz ES类型</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@param</span> qo 查询对象</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@param</span> fields 检索域</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@param</span> &lt;T&gt; 泛型展位</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@return</span> 带有分页的全文搜索(高亮显示)结果集</span></span><br><span class="line"><span class="comment">  */</span></span><br><span class="line">&lt;T&gt; Page&lt;T&gt;  <span class="title function_">searchWithHighlight</span><span class="params">(String index, Class&lt;T&gt; clz, SearchQueryObject qo, String... fields)</span>;</span><br></pre></td></tr></table></figure>
<p>实现这个方法：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"> <span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> ElasticsearchRestTemplate template;</span><br><span class="line"></span><br><span class="line"><span class="comment">//关键字: keyword = 广州</span></span><br><span class="line"><span class="comment">//以title为例:</span></span><br><span class="line"><span class="comment">//原始匹配: &quot;有娃必看,广州长隆野生动物园全攻略&quot;</span></span><br><span class="line"><span class="comment">//高亮显示后:&quot;有娃必看,&lt;span style=&quot;color:red;&quot;&gt;广州&lt;/span&gt;长隆野生动物园全攻略&quot;</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; Page&lt;T&gt; <span class="title function_">searchWithHighlight</span><span class="params">(String index, Class&lt;T&gt; clz, SearchQueryObject qo, String... fields)</span> &#123;</span><br><span class="line">    <span class="type">SearchRequest</span> <span class="variable">searchRequest</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SearchRequest</span>(index);</span><br><span class="line">    <span class="type">SearchSourceBuilder</span> <span class="variable">searchSourceBuilder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SearchSourceBuilder</span>();</span><br><span class="line">    <span class="comment">//高亮显示</span></span><br><span class="line">    <span class="comment">/*&quot;query&quot;:&#123;</span></span><br><span class="line"><span class="comment">            &quot;multi_match&quot;: &#123;</span></span><br><span class="line"><span class="comment">                &quot;query&quot;: &quot;广州&quot;,</span></span><br><span class="line"><span class="comment">                &quot;fields&quot;: [&quot;title&quot;,&quot;subTitle&quot;,&quot;summary&quot;]</span></span><br><span class="line"><span class="comment">            &#125;</span></span><br><span class="line"><span class="comment">        &#125;,*/</span></span><br><span class="line">    <span class="type">MultiMatchQueryBuilder</span> <span class="variable">queryBuilder</span> <span class="operator">=</span> QueryBuilders.multiMatchQuery(qo.getKeyword(),fields);</span><br><span class="line">    <span class="type">HighlightBuilder</span> <span class="variable">highlightBuilder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HighlightBuilder</span>(); <span class="comment">// 生成高亮查询器</span></span><br><span class="line">    <span class="keyword">for</span> (String field: fields) &#123;</span><br><span class="line">        highlightBuilder.field(field);<span class="comment">// 高亮查询字段</span></span><br><span class="line">    &#125;</span><br><span class="line">    highlightBuilder.requireFieldMatch(<span class="literal">false</span>); <span class="comment">// 如果要多个字段高亮,这项要为false</span></span><br><span class="line">    highlightBuilder.preTags(<span class="string">&quot;&lt;span style=&#x27;color:red&#x27;&gt;&quot;</span>); <span class="comment">// 高亮设置</span></span><br><span class="line">    highlightBuilder.postTags(<span class="string">&quot;&lt;/span&gt;&quot;</span>);</span><br><span class="line">    highlightBuilder.fragmentSize(<span class="number">800000</span>); <span class="comment">// 最大高亮分片数</span></span><br><span class="line">    highlightBuilder.numOfFragments(<span class="number">0</span>); <span class="comment">// 从第一个分片获取高亮片段</span></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">         &quot;from&quot;: 0,</span></span><br><span class="line"><span class="comment">         &quot;size&quot;:3,</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">    <span class="type">Pageable</span> <span class="variable">pageable</span> <span class="operator">=</span> PageRequest.of(qo.getCurrentPage() - <span class="number">1</span>, qo.getPageSize(),</span><br><span class="line">                                       Sort.Direction.ASC, <span class="string">&quot;_id&quot;</span>);<span class="comment">// 设置分页参数</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="type">NativeSearchQuery</span> <span class="variable">searchQuery</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">NativeSearchQueryBuilder</span>()</span><br><span class="line">        .withQuery(queryBuilder) <span class="comment">// match查询</span></span><br><span class="line">        .withHighlightBuilder(highlightBuilder) <span class="comment">// 设置高亮</span></span><br><span class="line"></span><br><span class="line">        .withPageable(pageable)</span><br><span class="line">        .build();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    SearchHits&lt;T&gt; searchHits = template.search(searchQuery, clz,IndexCoordinates.of(index));</span><br><span class="line">    List&lt;T&gt; list = <span class="keyword">new</span> <span class="title class_">ArrayList</span>();</span><br><span class="line">    <span class="keyword">for</span> (SearchHit&lt;T&gt; searchHit : searchHits) &#123; <span class="comment">// 获取搜索到的数据</span></span><br><span class="line">        <span class="type">T</span> <span class="variable">content</span> <span class="operator">=</span> <span class="built_in">this</span>.parseType(clz, searchHit.getId());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 处理高亮</span></span><br><span class="line">        Map&lt;String, String&gt; map = highlightFieldsCopy(searchHit.getHighlightFields(), fields);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//1：spring 框架中BeanUtils 类，如果是map集合是无法进行属性复制</span></span><br><span class="line">        <span class="comment">//   copyProperties(源， 目标)</span></span><br><span class="line">        <span class="comment">//2: apache  BeanUtils 类 可以进map集合属性复制</span></span><br><span class="line">        <span class="comment">//   copyProperties(目标， 源)</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            BeanUtils.copyProperties(content, map);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IllegalAccessException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InvocationTargetException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        list.add(content);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">Page</span> <span class="variable">page</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PageImpl</span>(list, pageable, searchHits.getTotalHits());</span><br><span class="line">    <span class="keyword">return</span>  page;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//判断类型并填充数据</span></span><br><span class="line"><span class="comment">//这里是将ES domain类型转换为 mysql domain类型，因为前端需要展示查询字段以外的信息。</span></span><br><span class="line"><span class="keyword">private</span> &lt;T&gt; T <span class="title function_">parseType</span><span class="params">(Class&lt;T&gt; clz, String id)</span>&#123;</span><br><span class="line">    <span class="type">Long</span> <span class="variable">lId</span> <span class="operator">=</span> <span class="number">0L</span>;</span><br><span class="line">    <span class="keyword">if</span>(StringUtils.hasLength(id))&#123;</span><br><span class="line">        lId = Long.valueOf(id);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">T</span> <span class="variable">t</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">if</span>(clz == UserInfo.class)&#123;</span><br><span class="line">        t = (T) userInfoService.getById(lId);</span><br><span class="line">    &#125;<span class="keyword">else</span> <span class="keyword">if</span>(clz == Travel.class)&#123;</span><br><span class="line">        t = (T) travelService.getById(lId);</span><br><span class="line"></span><br><span class="line">    &#125;<span class="keyword">else</span> <span class="keyword">if</span>(clz == Strategy.class)&#123;</span><br><span class="line">        t = (T) strategyService.getById(lId);</span><br><span class="line">    &#125;<span class="keyword">else</span> <span class="keyword">if</span>(clz == Destination.class)&#123;</span><br><span class="line">        t = (T) destinationService.getById(lId);</span><br><span class="line">    &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">        t= <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> t;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//fields: title subTitle summary</span></span><br><span class="line"><span class="keyword">private</span> Map&lt;String, String&gt;   <span class="title function_">highlightFieldsCopy</span><span class="params">(Map&lt;String, List&lt;String&gt;&gt; map, String ...fields)</span>&#123;</span><br><span class="line">    Map&lt;String, String&gt; mm = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    <span class="comment">//title:  &quot;有娃必看，&lt;span style=&#x27;color:red;&#x27;&gt;广州&lt;/span&gt;长隆野生动物园全攻略&quot;</span></span><br><span class="line">    <span class="comment">//subTitle: &quot;&lt;span style=&#x27;color:red;&#x27;&gt;广州&lt;/span&gt;长隆野生动物园&quot;</span></span><br><span class="line">    <span class="comment">//summary: &quot;如果要说动物园，楼主强烈推荐带娃去&lt;span style=&#x27;color:red;&#x27;&gt;广州&lt;/span&gt;长隆野生动物园</span></span><br><span class="line">    <span class="comment">//title subTitle summary</span></span><br><span class="line">    <span class="keyword">for</span> (String field : fields) &#123;</span><br><span class="line">        List&lt;String&gt; hfs = map.get(field);</span><br><span class="line">        <span class="keyword">if</span> (hfs != <span class="literal">null</span> &amp;&amp; !hfs.isEmpty()) &#123;</span><br><span class="line">            <span class="comment">//获取高亮显示字段值, 因为是一个数组, 所有使用string拼接</span></span><br><span class="line"></span><br><span class="line">            <span class="type">StringBuilder</span> <span class="variable">sb</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line">            <span class="keyword">for</span> (String hf : hfs) &#123;</span><br><span class="line">                sb.append(hf);</span><br><span class="line">            &#125;</span><br><span class="line">            mm.put(field, sb.toString());  <span class="comment">//使用map对象将所有能替换字段先缓存, 后续统一替换</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> mm;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>于是，在Controller中就能够通过它实现全站检索：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SearchController</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> DestinationService destinationService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> ITravelService travelService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> StrategyService strategyService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserinfoService userinfoService;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> ISearchService searchService;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/q&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> JsonResult <span class="title function_">search</span><span class="params">(SearchQueryObject qo)</span> <span class="keyword">throws</span> UnsupportedEncodingException &#123;</span><br><span class="line">        <span class="comment">//解码url中文参数</span></span><br><span class="line">        qo.setKeyword(URLDecoder.decode(qo.getKeyword(),<span class="string">&quot;UTF-8&quot;</span>));</span><br><span class="line">		</span><br><span class="line">        <span class="comment">//判断检索的类型，返回指定的检索结果</span></span><br><span class="line">        <span class="keyword">switch</span>(qo.getType())&#123;</span><br><span class="line">            <span class="keyword">case</span> SearchQueryObject.TYPE_DEST:<span class="keyword">return</span> <span class="built_in">this</span>.searchDest(qo);</span><br><span class="line">            <span class="keyword">case</span> SearchQueryObject.TYPE_STRATEGY:<span class="keyword">return</span> <span class="built_in">this</span>.searchStrategy(qo);</span><br><span class="line">            <span class="keyword">case</span> SearchQueryObject.TYPE_TRAVEL:<span class="keyword">return</span> <span class="built_in">this</span>.searchTravel(qo);</span><br><span class="line">            <span class="keyword">case</span> SearchQueryObject.TYPE_USER:<span class="keyword">return</span> <span class="built_in">this</span>.searchUser(qo);</span><br><span class="line">            <span class="keyword">default</span>:<span class="keyword">return</span> <span class="built_in">this</span>.searchAll(qo);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">	</span><br><span class="line">    <span class="comment">//检索所有</span></span><br><span class="line">    <span class="keyword">private</span> JsonResult <span class="title function_">searchAll</span><span class="params">(SearchQueryObject qo)</span> &#123;</span><br><span class="line">        SearchResultVo vo=<span class="keyword">new</span> <span class="title class_">SearchResultVo</span>();</span><br><span class="line">        <span class="comment">//dest</span></span><br><span class="line">        Page&lt;Destination&gt; ds = searchService.searchWithHighlight(DestinationEs.INDEX_NAME, Destination.class, qo, <span class="string">&quot;name&quot;</span>,<span class="string">&quot;info&quot;</span>);</span><br><span class="line">        vo.setDests(ds.getContent());</span><br><span class="line">        <span class="comment">//游记</span></span><br><span class="line">        Page&lt;Travel&gt; ts = searchService.searchWithHighlight(TravelEs.INDEX_NAME, Travel.class, qo, <span class="string">&quot;title&quot;</span>,  <span class="string">&quot;summary&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (Travel travel:ts)&#123;</span><br><span class="line">            travel.setAuthor(userinfoService.getById(travel.getAuthorId()));</span><br><span class="line">        &#125;</span><br><span class="line">        vo.setTravels(ts.getContent());</span><br><span class="line">        <span class="comment">//攻略</span></span><br><span class="line">        Page&lt;Strategy&gt; sts = searchService.searchWithHighlight(StrategyEs.INDEX_NAME, Strategy.class, qo, <span class="string">&quot;title&quot;</span>, <span class="string">&quot;subTitle&quot;</span>, <span class="string">&quot;summary&quot;</span>);</span><br><span class="line">        vo.setStrategys(sts.getContent());</span><br><span class="line">        <span class="comment">//用户</span></span><br><span class="line">        Page&lt;UserInfo&gt; us = searchService.searchWithHighlight(UserInfoEs.INDEX_NAME, UserInfo.class, qo, <span class="string">&quot;info&quot;</span>, <span class="string">&quot;city&quot;</span>);</span><br><span class="line">        vo.setUsers(us.getContent());</span><br><span class="line">        <span class="comment">//总数</span></span><br><span class="line">        vo.setTotal(ds.getTotalElements()+ts.getTotalElements()+sts.getTotalElements()+us.getTotalElements());</span><br><span class="line">        <span class="keyword">return</span> JsonResult.success(ParamMap.newInstance().put(<span class="string">&quot;result&quot;</span>,vo).put(<span class="string">&quot;qo&quot;</span>,qo));</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">	<span class="comment">//检索用户</span></span><br><span class="line">    <span class="keyword">private</span> JsonResult <span class="title function_">searchUser</span><span class="params">(SearchQueryObject qo)</span> &#123;</span><br><span class="line">        <span class="comment">//用户的全文搜索 + 高亮显示</span></span><br><span class="line">        Page&lt;UserInfo&gt; page = searchService.searchWithHighlight(UserInfoEs.INDEX_NAME, UserInfo.class, qo, <span class="string">&quot;info&quot;</span>, <span class="string">&quot;city&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> JsonResult.success(ParamMap.newInstance().put(<span class="string">&quot;page&quot;</span>,page).put(<span class="string">&quot;qo&quot;</span>,qo));</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">	<span class="comment">//检索游记</span></span><br><span class="line">    <span class="keyword">private</span> JsonResult <span class="title function_">searchTravel</span><span class="params">(SearchQueryObject qo)</span> &#123;</span><br><span class="line">        <span class="comment">//游记的全文搜索 + 高亮显示</span></span><br><span class="line">        Page&lt;Travel&gt; page = searchService.searchWithHighlight(TravelEs.INDEX_NAME, Travel.class, qo, <span class="string">&quot;title&quot;</span>,  <span class="string">&quot;summary&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (Travel travel:page)&#123;</span><br><span class="line">            travel.setAuthor(userinfoService.getById(travel.getAuthorId()));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> JsonResult.success(ParamMap.newInstance().put(<span class="string">&quot;page&quot;</span>,page).put(<span class="string">&quot;qo&quot;</span>,qo));</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">	<span class="comment">//检索攻略</span></span><br><span class="line">    <span class="keyword">private</span> JsonResult <span class="title function_">searchStrategy</span><span class="params">(SearchQueryObject qo)</span> &#123;</span><br><span class="line">        <span class="comment">//攻略的全文搜索 + 高亮显示</span></span><br><span class="line">        Page&lt;Strategy&gt; page = searchService.searchWithHighlight(StrategyEs.INDEX_NAME, Strategy.class, qo, <span class="string">&quot;title&quot;</span>, <span class="string">&quot;subTitle&quot;</span>, <span class="string">&quot;summary&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> JsonResult.success(ParamMap.newInstance().put(<span class="string">&quot;page&quot;</span>,page).put(<span class="string">&quot;qo&quot;</span>,qo));</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">	<span class="comment">//检索目的地</span></span><br><span class="line">    <span class="keyword">private</span> JsonResult <span class="title function_">searchDest</span><span class="params">(SearchQueryObject qo)</span> &#123;</span><br><span class="line">        <span class="comment">//通过keyword查询目的地</span></span><br><span class="line">        Destination dest=destinationService.queryByName(qo.getKeyword());</span><br><span class="line">        SearchResultVo rs=<span class="keyword">new</span> <span class="title class_">SearchResultVo</span>();</span><br><span class="line">        <span class="comment">//判断目的地是否存在 如果存在，查询目的地下所有的攻略、游记、用户</span></span><br><span class="line">        <span class="keyword">if</span>(dest!=<span class="literal">null</span>)&#123;</span><br><span class="line">           List&lt;Travel&gt; ts= travelService.queryByDestId(dest.getId());</span><br><span class="line">           List&lt;Strategy&gt; sts=strategyService.queryByDestId(dest.getId());</span><br><span class="line">           List&lt;UserInfo&gt; us=userinfoService.queryByCity(dest.getName());</span><br><span class="line"></span><br><span class="line">           rs.setUsers(us);</span><br><span class="line">           rs.setStrategys(sts);</span><br><span class="line">           rs.setTravels(ts);</span><br><span class="line">           rs.setTotal(ts.size()+sts.size()+us.size()+<span class="number">0L</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> JsonResult.success(ParamMap.newInstance().put(<span class="string">&quot;result&quot;</span>,rs).put(<span class="string">&quot;dest&quot;</span>,dest).put(<span class="string">&quot;qo&quot;</span>,qo));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="接口文档与安全">接口文档与安全</h2>
<div class="note info simple"><p>接口文档与安全问题一般出现在前后端分离项目中。</p>
<p>前后端分离的好处：责任分离</p>
<p>前端：负责页面开发与页面交互-web工程师</p>
<p>后端：负责业务接口开发，java后端工程师</p>
</div>
<h3 id="接口文档">接口文档</h3>
<h4 id="前言">前言</h4>
<p><strong>需求会议：</strong></p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">前端工程师：开发页面----|</span><br><span class="line"></span><br><span class="line">					|-----(一定进度后)联合调试----|</span><br><span class="line"></span><br><span class="line">后端工程师：开发接口----|                         |</span><br><span class="line"></span><br><span class="line">测试工程师：测试案例------------------------------|----封版----上线</span><br></pre></td></tr></table></figure>
<p><em>此时前后端的联合开发、调试就需要接口文档</em></p>
<h4 id="swagger2使用">swagger2使用</h4>
<p>首先，引入swagger2的依赖</p>
<figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.springfox<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>springfox-swagger2<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.9.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.springfox<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>springfox-swagger-ui<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.9.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>
<p>然后新建swagger2包，新建SwaggerConfig类</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableSwagger2</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SwaggerConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Docket <span class="title function_">productApi</span><span class="params">()</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//添加head参数start</span></span><br><span class="line">        <span class="type">ParameterBuilder</span> <span class="variable">tokenPar</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ParameterBuilder</span>();</span><br><span class="line">        List&lt;Parameter&gt; pars = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;Parameter&gt;();</span><br><span class="line">        tokenPar.name(<span class="string">&quot;token&quot;</span>).description(<span class="string">&quot;令牌&quot;</span>).modelRef(<span class="keyword">new</span> <span class="title class_">ModelRef</span>(<span class="string">&quot;string&quot;</span>)).parameterType(<span class="string">&quot;header&quot;</span>).required(<span class="literal">false</span>).build();</span><br><span class="line">        pars.add(tokenPar.build());</span><br><span class="line"></span><br><span class="line">        <span class="comment">//controller的包路径名称</span></span><br><span class="line">        String packageRoute=<span class="string">&quot;cn.wolfcode.wolf2w.controller&quot;</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Docket</span>(DocumentationType.SWAGGER_2).select()</span><br><span class="line">                <span class="comment">// 扫描的包路径</span></span><br><span class="line">                .apis(RequestHandlerSelectors.basePackage(packageRoute))</span><br><span class="line">                <span class="comment">// 定义要生成文档的Api的url路径规则</span></span><br><span class="line">                .paths(PathSelectors.any())</span><br><span class="line">                .build()</span><br><span class="line">                .globalOperationParameters(pars)</span><br><span class="line">                <span class="comment">// 设置swagger-ui.html页面上的一些元素信息。</span></span><br><span class="line">                .apiInfo(metaData());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> ApiInfo <span class="title function_">metaData</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">ApiInfoBuilder</span>()</span><br><span class="line">                <span class="comment">// 标题</span></span><br><span class="line">                .title(<span class="string">&quot;SpringBoot集成Swagger2&quot;</span>)</span><br><span class="line">                <span class="comment">// 描述</span></span><br><span class="line">                .description(<span class="string">&quot;狼行天下项目接口文档&quot;</span>)</span><br><span class="line">                <span class="comment">// 文档版本</span></span><br><span class="line">                .version(<span class="string">&quot;1.0.0&quot;</span>)</span><br><span class="line">                .license(<span class="string">&quot;Apache License Version 2.0&quot;</span>)</span><br><span class="line">                .licenseUrl(<span class="string">&quot;https://www.apache.org/licenses/LICENSE-2.0&quot;</span>)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//ui页面</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addResourceHandlers</span><span class="params">(ResourceHandlerRegistry registry)</span> &#123;</span><br><span class="line">        registry.addResourceHandler(<span class="string">&quot;swagger-ui.html&quot;</span>)</span><br><span class="line">                .addResourceLocations(<span class="string">&quot;classpath:/META-INF/resources/&quot;</span>);</span><br><span class="line"></span><br><span class="line">        registry.addResourceHandler(<span class="string">&quot;/webjars/**&quot;</span>)</span><br><span class="line">                .addResourceLocations(<span class="string">&quot;classpath:/META-INF/resources/webjars/&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>启动后如果出现 <code>Failed to start bean 'documentationPluginsBootstrapper'</code> 错误，需要在 <code>application.yml</code> 中进行如下配置：</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">mvc:</span></span><br><span class="line">    <span class="attr">pathmatch:</span></span><br><span class="line">      <span class="attr">matching-strategy:</span> <span class="string">ant_path_matcher</span></span><br></pre></td></tr></table></figure>
<p>再次启动后，访问 <code>http://localhost:port/swagger-ui.html</code> 即可查看和接口文档</p>
<p>接口文档为英文且缺少说明文字，可以添加一些注解帮助生成的文档更加详细：</p>
<ol>
<li class="lvl-3">
<p>关于控制器的说明</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Api(value = &quot;用户资源&quot;,tags = &quot;用户控制器&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserInfoController</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li class="lvl-3">
<p>关于接口方法的说明</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@ApiOperation(value = &quot;账号检测&quot;,notes=&quot;查看当前电话是否已经注册&quot;)</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/checkPhone&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Boolean <span class="title function_">checkPhone</span><span class="params">(String phone)</span>&#123;</span><br><span class="line">    <span class="type">boolean</span> ret=userinfoService.checkPhone(phone);</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li class="lvl-3">
<p>关于接口方法请求参数的说明</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@ApiOperation(value = &quot;账号检测&quot;,notes=&quot;查看当前电话是否已经注册&quot;)</span></span><br><span class="line"><span class="meta">@ApiImplicitParams(&#123;</span></span><br><span class="line"><span class="meta"> 	@ApiImplicitParam(value = &quot;手机号&quot;,name=&quot;phone&quot;,dataType = &quot;String&quot;,required = true), //这里可以添加多个，用逗号隔开</span></span><br><span class="line"><span class="meta">&#125;)</span></span><br><span class="line"><span class="meta">@GetMapping(&quot;/checkPhone&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Boolean <span class="title function_">checkPhone</span><span class="params">(String phone)</span>&#123;</span><br><span class="line">    <span class="type">boolean</span> ret=userinfoService.checkPhone(phone);</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li class="lvl-3">
<p>关于响应的说明</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/currentUser&quot;)</span></span><br><span class="line"><span class="meta">@ApiResponse(code = 200,message = &quot;获取当前用户成功&quot;)</span></span><br><span class="line"><span class="keyword">public</span> JsonResult <span class="title function_">getCurrentUser</span><span class="params">(<span class="meta">@UserParam</span> UserInfo user)</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> JsonResult.success(user);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li class="lvl-3">
<p>当请求参数为对象时，可以直接在对应参数上添加说明注解</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* 游记查询参数封装对象</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="meta">@Setter</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@ApiModel(value=&quot;游记查询&quot;,description=&quot;游记查询参数&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TravelQuery</span> <span class="keyword">extends</span>  <span class="title class_">QueryObject</span>&#123;</span><br><span class="line">    <span class="keyword">private</span> Long destId;</span><br><span class="line">    <span class="meta">@ApiModelProperty(value=&quot;天数类型&quot;,name=&quot;dayType&quot;,dataType=&quot;Integer&quot;,requird=true)</span></span><br><span class="line">    <span class="keyword">private</span> Integer dayType;</span><br><span class="line">    <span class="keyword">private</span> Integer travelTimeType;</span><br><span class="line">    <span class="keyword">private</span> Integer consumeType;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
</ol>
<div class="note warning simple"><p>但是个人认为swagger的接口文档很丑且不好查阅。而swagger+Knife4j似乎体验更佳，因此之后会再开一个坑来专门学习Knife4j。</p>
</div>
<h3 id="接口安全">接口安全</h3>
<div class="note info simple"><p>接口安全要求：</p>
<ol>
<li class="lvl-3">防伪装攻击（如在公共网络中，第三方恶意调用接口）：接口防刷</li>
<li class="lvl-3">防篡改攻击（如在公共网络环境中，请求头/查询字符/内容 在传输过程中被修改）：接口防篡改（签名机制）</li>
<li class="lvl-3">防重放攻击（如在公共网络中，请求被截获，稍后被重放或多次重放）：接口时效性</li>
<li class="lvl-3">防数据信息泄漏（案例：截获用户登录请求，截获到账号、密码等）：接口加密（https/对称加解密）</li>
</ol>
</div>
<h4 id="接口防刷">接口防刷</h4>
<p><strong>问题描述</strong>：不法分子超频访问某个接口，导致接口超频使用，影响正常请求处理，导致系统异常。此时需要对超频请求进行限制。</p>
<p>**核心：**如何定义超频访问。如限制10次/分钟，超过后算超频。</p>
<p>**注意：**计数器的key值需要以客户端和接口作为识别键，客户端使用ip，接口采用接口路径。 前缀:ip:url – value</p>
<p>**选择：**使用redis进行计数：快+时效需求；要在请求进入前处理，因此需要使用filter或interceptor实现。此处选择interceptor。</p>
<p><strong>思路：</strong></p>
<ol>
<li class="lvl-3">
<p>设计一个redis临时key，有效时间1分钟，1分钟内只允许访问10次。</p>
<p>key &gt; 前缀:url:ip</p>
<p>value &gt; 访问的次数</p>
</li>
<li class="lvl-3">
<p>设置拦截器，拦截需要防刷的接口url</p>
</li>
<li class="lvl-3">
<p>拦截逻辑：</p>
<ul class="lvl-2">
<li class="lvl-5">拦截url，拼接key查询redis中是否存在</li>
<li class="lvl-5">如果不存在 setnx url:ip 10</li>
<li class="lvl-5">如果存在 derc url:ip</li>
<li class="lvl-5">如果次数减到0，拦截返回，请勿频繁访问</li>
<li class="lvl-5">其他情况放行</li>
</ul>
</li>
</ol>
<p><strong>实现：</strong></p>
<p>首先，定义一个接口防刷拦截器：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Lenl</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@version</span> v1.0</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@create</span> 2023-05-02 10:01</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@description</span> 防刷拦截器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BrushProofInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> ISecurityRedisService securityRedisService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">if</span>(!(handler <span class="keyword">instanceof</span> HandlerMethod))&#123;</span><br><span class="line">            <span class="keyword">return</span>  <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//防刷验证</span></span><br><span class="line">        String url=request.getRequestURI().substring(<span class="number">1</span>);</span><br><span class="line">        String ip= RequestUtil.getIPAddress();</span><br><span class="line"></span><br><span class="line">        String key= RedisKeys.BRUSH_PROOF.join(url,ip);</span><br><span class="line">        <span class="keyword">if</span>(!securityRedisService.isAllowBrush(key))&#123;</span><br><span class="line">            response.setContentType(<span class="string">&quot;text/json;charset=UTF-8&quot;</span>);</span><br><span class="line">            response.getWriter().write(JSON.toJSONString(JsonResult.error(<span class="number">500</span>,<span class="string">&quot;请勿频繁操作！&quot;</span>,<span class="literal">null</span>)));</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>这里用到了自定义的一些工具类，如下：</p>
<p>RequestUtil.java:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RequestUtil</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">getIPAddress</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">HttpServletRequest</span> <span class="variable">request</span> <span class="operator">=</span> ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); ;</span><br><span class="line">        <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">        <span class="comment">//X-Forwarded-For：Squid 服务代理</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">ipAddresses</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;X-Forwarded-For&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (ipAddresses == <span class="literal">null</span> || ipAddresses.length() == <span class="number">0</span> || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ipAddresses)) &#123;</span><br><span class="line">            <span class="comment">//Proxy-Client-IP：apache 服务代理</span></span><br><span class="line">            ipAddresses = request.getHeader(<span class="string">&quot;Proxy-Client-IP&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (ipAddresses == <span class="literal">null</span> || ipAddresses.length() == <span class="number">0</span> || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ipAddresses)) &#123;</span><br><span class="line">            <span class="comment">//WL-Proxy-Client-IP：weblogic 服务代理</span></span><br><span class="line">            ipAddresses = request.getHeader(<span class="string">&quot;WL-Proxy-Client-IP&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (ipAddresses == <span class="literal">null</span> || ipAddresses.length() == <span class="number">0</span> || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ipAddresses)) &#123;</span><br><span class="line">            <span class="comment">//HTTP_CLIENT_IP：有些代理服务器</span></span><br><span class="line">            ipAddresses = request.getHeader(<span class="string">&quot;HTTP_CLIENT_IP&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (ipAddresses == <span class="literal">null</span> || ipAddresses.length() == <span class="number">0</span> || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ipAddresses)) &#123;</span><br><span class="line">            <span class="comment">//X-Real-IP：nginx服务代理</span></span><br><span class="line">            ipAddresses = request.getHeader(<span class="string">&quot;X-Real-IP&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//有些网络通过多层代理，那么获取到的ip就会有多个，一般都是通过逗号（,）分割开来，并且第一个ip为客户端的真实IP</span></span><br><span class="line">        <span class="keyword">if</span> (ipAddresses != <span class="literal">null</span> &amp;&amp; ipAddresses.length() != <span class="number">0</span>) &#123;</span><br><span class="line">            ip = ipAddresses.split(<span class="string">&quot;,&quot;</span>)[<span class="number">0</span>];</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//还是不能获取到，最后再通过request.getRemoteAddr();获取</span></span><br><span class="line">        <span class="keyword">if</span> (ip == <span class="literal">null</span> || ip.length() == <span class="number">0</span> || <span class="string">&quot;unknown&quot;</span>.equalsIgnoreCase(ipAddresses)) &#123;</span><br><span class="line">            ip = request.getRemoteAddr();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> ip;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>SecurityRedisService的isAllowBrush：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SecurityRedisServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">ISecurityRedisService</span> &#123;</span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate template;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">isAllowBrush</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="comment">//如果没有则添加</span></span><br><span class="line">        template.opsForValue().setIfAbsent(key,<span class="string">&quot;10&quot;</span>, RedisKeys.BRUSH_PROOF.getTime(), TimeUnit.SECONDS);</span><br><span class="line">        <span class="comment">//数量 -1</span></span><br><span class="line">        Long decrement=template.opsForValue().decrement(key);</span><br><span class="line">        <span class="keyword">return</span> decrement&gt;=<span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>关于RedisKeys是redis key的一些生成工具，可参照前面的关于Redis的笔记。</p>
<p>拦截器建成之后，在Config中添加并配置：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WebConfig</span> <span class="keyword">implements</span>  <span class="title class_">WebMvcConfigurer</span>&#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> BrushProofInterceptor <span class="title function_">brushProofInterceptor</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">BrushProofInterceptor</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        <span class="comment">//添加防刷拦截器</span></span><br><span class="line">        registry.addInterceptor(brushProofInterceptor())</span><br><span class="line">            <span class="comment">//所有的请求路径都进行拦截</span></span><br><span class="line">            .addPathPatterns(<span class="string">&quot;/**&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>再次访问接口时，频繁刷新到达10次则会禁止，具体数目可自行设置。</p>
<h4 id="接口防篡改">接口防篡改</h4>
<p>**问题描述：**正常请求被不法分子拦截，并篡改其中的请求参数，然后伪装成正常请求发送到服务端，服务端无法识别而当成正常请求处理导致不合法事情发生，此时需要进行防护。</p>
<p>**核心：**服务端如何识别参数已经被篡改：最优解为签名机制。</p>
<p>**签名机制：**客户端在发起请求之前们将所有的参数按照一定的规则拼接成字符串，然后对字符串进行加密得到一个密文（签名）：client_sign；客户端将所有参数与签名以期发送到服务端；服务端接收到参数与签名，使用相同算法对数据进行签名；比对签名如果一致，则数据没有被篡改，反之则已经被篡改。</p>
<p><strong>思路：</strong></p>
<ol>
<li class="lvl-3">
<p>前端传参时，对参数的名进行字典排序，然后按照参数名的属性对参数值进行有序拼接</p>
<p>如：</p>
<p>参数列表：c-参数1，f-参数2，a-参数3，b-参数4；</p>
<p>参数排名：a b c f</p>
<p>参数值拼接：a-参数3&amp;b-参数4&amp;c-参数1&amp;f-参数2</p>
</li>
<li class="lvl-3">
<p>使用MD5对拼接参数值串进行加密得到参数签名sign_client</p>
</li>
<li class="lvl-3">
<p>将所有参数与sign一同发送到后端</p>
</li>
<li class="lvl-3">
<p>后端获取到所有参数，按照相同的逻辑，MD5加密得到sign_server</p>
</li>
<li class="lvl-3">
<p>对比sign_server跟sign_client2个签名是否一致，一致则表示参数没有变更篡改，否则参数被改，不合法。</p>
</li>
</ol>
<p><strong>实现：</strong></p>
<p>首先，前端发起请求需要对参数进行一个签名：</p>
<figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">getSignString</span>(<span class="params">param</span>) &#123;</span><br><span class="line">    <span class="comment">//默认按字典顺序排序</span></span><br><span class="line">    <span class="keyword">var</span> sdic=<span class="title class_">Object</span>.<span class="title function_">keys</span>(param).<span class="title function_">sort</span>();</span><br><span class="line">    <span class="keyword">var</span> signStr = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">var</span> i <span class="keyword">in</span> sdic)&#123;</span><br><span class="line">        <span class="keyword">if</span>(i == <span class="number">0</span>)&#123;</span><br><span class="line">            signStr +=sdic[i]+<span class="string">&quot;=&quot;</span>+param[sdic[i]];</span><br><span class="line">        &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">            signStr +=<span class="string">&quot;&amp;&quot;</span>+sdic[i]+<span class="string">&quot;=&quot;</span>+param[sdic[i]];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">hex_md5</span>(signStr).<span class="title function_">toUpperCase</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>每次发起请求时带上这个sign，在后端同样使用拦截器的方式进行防护：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Lenl</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@version</span> v1.0</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@create</span> 2023-05-02 11:47</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@description</span> 签名拦截（防篡改）</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SignInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="keyword">if</span>(!(handler <span class="keyword">instanceof</span> HandlerMethod))&#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//签名验证</span></span><br><span class="line">        Map&lt;String,String[]&gt; map=request.getParameterMap();</span><br><span class="line">        Set&lt;String&gt; keys=map.keySet();</span><br><span class="line">        Map&lt;String,Object&gt; param=<span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (String s:map.keySet())&#123;</span><br><span class="line">            <span class="keyword">if</span>(<span class="string">&quot;sign&quot;</span>.equalsIgnoreCase(s))&#123;</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            param.put(s,arrayToString(map.get(s)));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//server_sign</span></span><br><span class="line">        String signatures= Md5Utils.signatures(param);</span><br><span class="line">        <span class="comment">//client_sign</span></span><br><span class="line">        String sign=request.getParameter(<span class="string">&quot;sign&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span>(sign==<span class="literal">null</span>||!sign.equalsIgnoreCase(signatures))&#123;</span><br><span class="line">            response.setContentType(<span class="string">&quot;text/json;charset=UTF-8&quot;</span>);</span><br><span class="line">            response.getWriter().write(JSON.toJSONString(<span class="keyword">new</span> <span class="title class_">JsonResult</span>(<span class="number">501</span>,<span class="string">&quot;签名校验失败&quot;</span>,<span class="literal">null</span>)));</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">arrayToString</span><span class="params">(String[] array)</span> &#123;</span><br><span class="line">        StringBuilder sb=<span class="keyword">new</span> <span class="title class_">StringBuilder</span>(<span class="number">10</span>);</span><br><span class="line">        <span class="keyword">for</span> (String s:array)&#123;</span><br><span class="line">            sb.append(s);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> sb.toString();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>这里涉及到一些工具类：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Md5Utils</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@Description</span>: 签名：请求参数排序并后面补充key值，最后进行MD5加密，返回大写结果</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> params 参数内容</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">signatures</span><span class="params">(Map&lt;String, Object&gt; params)</span>&#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">signatures</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            List&lt;String&gt; paramsStr = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;();</span><br><span class="line">            <span class="keyword">for</span> (String key1 : params.keySet()) &#123;</span><br><span class="line">                <span class="keyword">if</span>(<span class="literal">null</span> != key1 &amp;&amp; !<span class="string">&quot;&quot;</span>.equals(key1))&#123;</span><br><span class="line">                    paramsStr.add(key1);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            Collections.sort(paramsStr);</span><br><span class="line">            <span class="type">StringBuilder</span> <span class="variable">sbff</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line">            <span class="keyword">for</span> (String kk : paramsStr) &#123;</span><br><span class="line">                <span class="type">String</span> <span class="variable">value</span> <span class="operator">=</span> params.get(kk).toString();</span><br><span class="line">                <span class="keyword">if</span> (<span class="string">&quot;&quot;</span>.equals(sbff.toString())) &#123;</span><br><span class="line">                    sbff.append(kk + <span class="string">&quot;=&quot;</span> + value);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    sbff.append(<span class="string">&quot;&amp;&quot;</span> + kk + <span class="string">&quot;=&quot;</span> + value);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//加上key值</span></span><br><span class="line">            signatures = getMD5(sbff.toString()).toUpperCase();</span><br><span class="line">        &#125;<span class="keyword">catch</span>(Exception e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> signatures;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@Description</span>: 生成MD5</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> message</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">getMD5</span><span class="params">(String message)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">md5</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">MessageDigest</span> <span class="variable">md</span> <span class="operator">=</span> MessageDigest.getInstance(<span class="string">&quot;MD5&quot;</span>); <span class="comment">// 创建一个md5算法对象</span></span><br><span class="line">            <span class="type">byte</span>[] messageByte = message.getBytes(<span class="string">&quot;UTF-8&quot;</span>);</span><br><span class="line">            <span class="type">byte</span>[] md5Byte = md.digest(messageByte); <span class="comment">// 获得MD5字节数组,16*8=128位</span></span><br><span class="line">            md5 = bytesToHex(md5Byte); <span class="comment">// 转换为16进制字符串</span></span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> md5;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@Description</span>: 二进制转十六进制</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> bytes</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> String <span class="title function_">bytesToHex</span><span class="params">(<span class="type">byte</span>[] bytes)</span> &#123;</span><br><span class="line">        <span class="type">StringBuffer</span> <span class="variable">hexStr</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuffer</span>();</span><br><span class="line">        <span class="type">int</span> num;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; bytes.length; i++) &#123;</span><br><span class="line">            num = bytes[i];</span><br><span class="line">            <span class="keyword">if</span> (num &lt; <span class="number">0</span>) &#123;</span><br><span class="line">                num += <span class="number">256</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (num &lt; <span class="number">16</span>) &#123;</span><br><span class="line">                hexStr.append(<span class="string">&quot;0&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            hexStr.append(Integer.toHexString(num));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> hexStr.toString().toUpperCase();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>拦截器建成之后，在Config中添加并配置：</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WebConfig</span> <span class="keyword">implements</span>  <span class="title class_">WebMvcConfigurer</span>&#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> SignInterceptor <span class="title function_">signInterceptor</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SignInterceptor</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        <span class="comment">//添加签名拦截器（防篡改）</span></span><br><span class="line">        registry.addInterceptor(signInterceptor())</span><br><span class="line">                .addPathPatterns(<span class="string">&quot;/**&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<div class="note warning simple"><p>签名机制操作需要注意的细节：</p>
<ol>
<li class="lvl-3">如果接口为文件上传，接口需要额外处理。</li>
<li class="lvl-3">如果接口参数是一个大体积参数，需要额外处理</li>
<li class="lvl-3">签名算法不能外泄–需要对前端签名算法进行加密混淆。</li>
</ol>
</div>
<h4 id="接口时效性">接口时效性</h4>
<p>**描述：**一个请求路径+参数在一定时间内可以重复请求，但是一旦超时，不允许访问。经典用法为：具有时效的二维码。</p>
<p>**核心：**如何判断该请求是否超时。</p>
<p><strong>思路：</strong></p>
<ol>
<li class="lvl-3">
<p>前端传参数时，对参数的名字进行字段排序，然后按参数名的属性对参数值进行有序拼接：</p>
<p>参数列表：c=参数1，f=参数2，a=参数3，b=参数4，timestamp=19999999</p>
<p>参数排名：a b c f timestamp</p>
<p>参数值拼接：a=参数3&amp;b=参数4&amp;c=参数1&amp;f=参数2&amp;timestamp=19999999</p>
</li>
<li class="lvl-3">
<p>使用MD5拼接参数值串加密得到的参数签名sign_client</p>
</li>
<li class="lvl-3">
<p>将所有参数与sign一同发送到后端</p>
</li>
<li class="lvl-3">
<p>后端获取所有参数，按照同样的逻辑，MD5加密得到sign_server</p>
</li>
<li class="lvl-3">
<p>获取timestamp与当前时间对比是否在有效时间内。不在则提示接口访问超时。</p>
</li>
<li class="lvl-3">
<p>对比sign_server与sign_clent2个签名是否一致，一致表示参数没有被篡改，否则参数不合法。</p>
</li>
</ol>
<div class="note info simple"><p>该方法与接口防篡改方法重合度高，此处不再提供参考。</p>
</div>
<h4 id="接口加密">接口加密</h4>
<p>https = http + ssl</p>
<p>SpringBoot可以生成本地SSL假证书 进行测试。详情可参照 <a target="_blank" rel="noopener" href="https://blog.csdn.net/zyx1260168395/article/details/112802464">文章链接</a></p>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta">文章作者: </span><span class="post-copyright-info"><a href="https://lienle.gitee.io">Lenl</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta">文章链接: </span><span class="post-copyright-info"><a href="https://lienle.gitee.io/notes/wolf2w_2/">https://lienle.gitee.io/notes/wolf2w_2/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta">版权声明: </span><span class="post-copyright-info">本博客所有文章除特别声明外，均采用 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> 许可协议。转载请注明来自 <a href="https://lienle.gitee.io" target="_blank">LenlBlog</a>！</span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/tags/SpringBoot/">SpringBoot</a><a class="post-meta__tags" href="/tags/ElasticSearch/">ElasticSearch</a><a class="post-meta__tags" href="/tags/%E6%8E%A5%E5%8F%A3%E5%AE%89%E5%85%A8/">接口安全</a></div><div class="post_share"><div class="social-share" data-image="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/css/share.min.css" media="print" onload="this.media='all'"><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/js/social-share.min.js" defer></script></div></div><div class="post-reward"><div class="reward-button"><i class="fas fa-qrcode"></i> 打赏</div><div class="reward-main"><ul class="reward-all"><li class="reward-item"><a href="/img/wechat.png" target="_blank"><img class="post-qr-code-img" src= "/img/hexo_loading.gif" data-lazy-src="/img/wechat.png" alt="微信"/></a><div class="post-qr-code-desc">微信</div></li><li class="reward-item"><a href="/img/alipay.png" target="_blank"><img class="post-qr-code-img" src= "/img/hexo_loading.gif" data-lazy-src="/img/alipay.png" alt="支付宝"/></a><div class="post-qr-code-desc">支付宝</div></li></ul></div></div><nav class="pagination-post" id="pagination"><div class="prev-post pull-left"><a href="/person/%E5%85%B3%E4%BA%8E%E5%A4%B4%E5%83%8F/" title="我做了一个专属形象-头像"><img class="cover" src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/23/lenl_01.md.png" onerror="onerror=null;src='/img/404.jpg'" alt="cover of previous post"><div class="pagination-info"><div class="label">上一篇</div><div class="prev_info">我做了一个专属形象-头像</div></div></a></div><div class="next-post pull-right"><a href="/notes/mongoDB/" title="MongoDB入门学习笔记"><img class="cover" src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" onerror="onerror=null;src='/img/404.jpg'" alt="cover of next post"><div class="pagination-info"><div class="label">下一篇</div><div class="next_info">MongoDB入门学习笔记</div></div></a></div></nav><div class="relatedPosts"><div class="headline"><i class="fas fa-thumbs-up fa-fw"></i><span>相关推荐</span></div><div class="relatedPosts-list"><div><a href="/notes/Knife4j/" title="SpringBoot2集成Knife4j完成api文档自动生成"><img class="cover" src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2023-05-03</div><div class="title">SpringBoot2集成Knife4j完成api文档自动生成</div></div></a></div><div><a href="/notes/PdfView/" title="基于pdf.js的前后端分离式PDF文件预览实现"><img class="cover" src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2023-08-06</div><div class="title">基于pdf.js的前后端分离式PDF文件预览实现</div></div></a></div><div><a href="/notes/chunkFile/" title="基于SpringBoot与Vue的大文件分片上传与流式下载"><img class="cover" src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2023-05-13</div><div class="title">基于SpringBoot与Vue的大文件分片上传与流式下载</div></div></a></div><div><a href="/notes/wolf2w/" title="wolf2w项目经验_缓存、持久化与参数解析器"><img class="cover" src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2023-04-20</div><div class="title">wolf2w项目经验_缓存、持久化与参数解析器</div></div></a></div><div><a href="/notes/tips/" title="SpringBoot一些优雅的小技巧记录"><img class="cover" src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" alt="cover"><div class="content is-center"><div class="date"><i class="far fa-calendar-alt fa-fw"></i> 2023-05-03</div><div class="title">SpringBoot一些优雅的小技巧记录</div></div></a></div></div></div><hr/><div id="post-comment"><div class="comment-head"><div class="comment-headline"><i class="fas fa-comments fa-fw"></i><span> 评论</span></div></div><div class="comment-wrap"><div><div class="vcomment" id="vcomment"></div></div></div></div></div><div class="aside-content" id="aside-content"><div class="card-widget card-info"><div class="is-center"><div class="avatar-img"><img src= "/img/hexo_loading.gif" data-lazy-src="/img/lenl.png" onerror="this.onerror=null;this.src='/img/friend_404.gif'" alt="avatar"/></div><div class="author-info__name">Lenl</div><div class="author-info__description">心有所向，日复一日，必有精进~</div></div><div class="card-info-data site-data is-center"><a href="/archives/"><div class="headline">文章</div><div class="length-num">17</div></a><a href="/tags/"><div class="headline">标签</div><div class="length-num">25</div></a><a href="/categories/"><div class="headline">分类</div><div class="length-num">8</div></a></div><a id="card-info-btn" target="_blank" rel="noopener" href="https://github.com/vimianma"><i class="fab fa-github"></i><span>View me</span></a><div class="card-info-social-icons is-center"><a class="social-icon" href="https://gitee.com/lienle" target="_blank" title="Gitee"><i class="fa-brands fa-git" style="color: #c71d23;"></i></a><a class="social-icon" href="https://github.com/vimianma" target="_blank" title="Github"><i class="fab fa-github" style="color: #24292e;"></i></a><a class="social-icon" href="mailto:lenl412@163.com" target="_blank" title="Email"><i class="fas fa-envelope" style="color: #4a7dbe;"></i></a></div></div><div class="card-widget card-announcement"><div class="item-headline"><i class="fas fa-bullhorn fa-shake"></i><span>公告</span></div><div class="announcement_content">本站意在记录个人生活中的所学所感所想所获；<br/>心有所向，日复一日，必有精进~~~<br>请多多指教~</div></div><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>目录</span><span class="toc-percentage"></span></div><div class="toc-content"><ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link"><span class="toc-number">1.</span> <span class="toc-text">wolf2w项目学习随记_2</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BD%BF%E7%94%A8ElasticSearch%E5%AE%9E%E7%8E%B0%E7%AB%99%E5%86%85%E6%90%9C%E7%B4%A2"><span class="toc-number">1.1.</span> <span class="toc-text">使用ElasticSearch实现站内搜索</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#1-%E6%95%B0%E6%8D%AE%E5%88%9D%E5%A7%8B%E5%8C%96%E5%87%86%E5%A4%87"><span class="toc-number">1.1.1.</span> <span class="toc-text">1 数据初始化准备</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#1-1-%E5%BC%95%E5%85%A5%E4%B8%8E%E9%85%8D%E7%BD%AE"><span class="toc-number">1.1.1.1.</span> <span class="toc-text">1.1 引入与配置</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#1-2-%E5%88%9B%E5%BB%BAes%E9%9C%80%E8%A6%81%E7%9A%84%E5%AF%B9%E5%BA%94%E7%9A%84domain%E5%AF%B9%E8%B1%A1"><span class="toc-number">1.1.1.2.</span> <span class="toc-text">1.2 创建es需要的对应的domain对象</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#1-3-%E7%94%9F%E6%88%90%E5%AF%B9%E5%BA%94%E7%9A%84Repository"><span class="toc-number">1.1.1.3.</span> <span class="toc-text">1.3 生成对应的Repository</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#1-4-%E7%94%9F%E6%88%90%E5%AF%B9%E5%BA%94%E7%9A%84service%E5%92%8Cimpl"><span class="toc-number">1.1.1.4.</span> <span class="toc-text">1.4 生成对应的service和impl</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#2-%E5%AE%89%E8%A3%85es"><span class="toc-number">1.1.2.</span> <span class="toc-text">2 安装es</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#2-1-%E4%B8%8B%E8%BD%BD%E5%AE%89%E8%A3%85es"><span class="toc-number">1.1.2.1.</span> <span class="toc-text">2.1 下载安装es</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#2-2-%E5%AE%89%E8%A3%85ik%E5%88%86%E8%AF%8D%E6%8F%92%E4%BB%B6"><span class="toc-number">1.1.2.2.</span> <span class="toc-text">2.2 安装ik分词插件</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#2-3-%E5%AE%89%E8%A3%85%E5%8F%AF%E8%A7%86%E5%8C%96%E6%8F%92%E4%BB%B6"><span class="toc-number">1.1.2.3.</span> <span class="toc-text">2.3 安装可视化插件</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#3-%E6%95%B0%E6%8D%AE%E7%BB%B4%E6%8A%A4"><span class="toc-number">1.1.3.</span> <span class="toc-text">3 数据维护</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#3-1-%E9%A6%96%E6%AC%A1%E5%88%9D%E5%A7%8B%E5%8C%96"><span class="toc-number">1.1.3.1.</span> <span class="toc-text">3.1 首次初始化</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#3-2-mysql%E4%B8%8Ees%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5"><span class="toc-number">1.1.3.2.</span> <span class="toc-text">3.2 mysql与es数据同步</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-%E5%85%A8%E6%96%87%E6%A3%80%E7%B4%A2%E5%AE%9E%E7%8E%B0"><span class="toc-number">1.1.4.</span> <span class="toc-text">4 全文检索实现</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E4%B8%8E%E5%AE%89%E5%85%A8"><span class="toc-number">1.2.</span> <span class="toc-text">接口文档与安全</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3"><span class="toc-number">1.2.1.</span> <span class="toc-text">接口文档</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E5%89%8D%E8%A8%80"><span class="toc-number">1.2.1.1.</span> <span class="toc-text">前言</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#swagger2%E4%BD%BF%E7%94%A8"><span class="toc-number">1.2.1.2.</span> <span class="toc-text">swagger2使用</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%8E%A5%E5%8F%A3%E5%AE%89%E5%85%A8"><span class="toc-number">1.2.2.</span> <span class="toc-text">接口安全</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%8E%A5%E5%8F%A3%E9%98%B2%E5%88%B7"><span class="toc-number">1.2.2.1.</span> <span class="toc-text">接口防刷</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%8E%A5%E5%8F%A3%E9%98%B2%E7%AF%A1%E6%94%B9"><span class="toc-number">1.2.2.2.</span> <span class="toc-text">接口防篡改</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%8E%A5%E5%8F%A3%E6%97%B6%E6%95%88%E6%80%A7"><span class="toc-number">1.2.2.3.</span> <span class="toc-text">接口时效性</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E6%8E%A5%E5%8F%A3%E5%8A%A0%E5%AF%86"><span class="toc-number">1.2.2.4.</span> <span class="toc-text">接口加密</span></a></li></ol></li></ol></li></ol></li></ol></div></div><div class="card-widget card-recent-post"><div class="item-headline"><i class="fas fa-history"></i><span>最新文章</span></div><div class="aside-list"><div class="aside-list-item"><a class="thumbnail" href="/notes/dockerNotes/" title="Docker备忘笔记"><img src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" onerror="this.onerror=null;this.src='/img/404.jpg'" alt="Docker备忘笔记"/></a><div class="content"><a class="title" href="/notes/dockerNotes/" title="Docker备忘笔记">Docker备忘笔记</a><time datetime="2024-04-06T13:17:09.000Z" title="发表于 2024-04-06 21:17:09">2024-04-06</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/notes/mysqlInstall/" title="mysql压缩包方式本地安装"><img src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" onerror="this.onerror=null;this.src='/img/404.jpg'" alt="mysql压缩包方式本地安装"/></a><div class="content"><a class="title" href="/notes/mysqlInstall/" title="mysql压缩包方式本地安装">mysql压缩包方式本地安装</a><time datetime="2024-01-03T05:33:33.000Z" title="发表于 2024-01-03 13:33:33">2024-01-03</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/notes/PdfView/" title="基于pdf.js的前后端分离式PDF文件预览实现"><img src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" onerror="this.onerror=null;this.src='/img/404.jpg'" alt="基于pdf.js的前后端分离式PDF文件预览实现"/></a><div class="content"><a class="title" href="/notes/PdfView/" title="基于pdf.js的前后端分离式PDF文件预览实现">基于pdf.js的前后端分离式PDF文件预览实现</a><time datetime="2023-08-06T02:18:26.000Z" title="发表于 2023-08-06 10:18:26">2023-08-06</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/notes/electronVue/" title="Electron+Vue项目搭建"><img src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" onerror="this.onerror=null;this.src='/img/404.jpg'" alt="Electron+Vue项目搭建"/></a><div class="content"><a class="title" href="/notes/electronVue/" title="Electron+Vue项目搭建">Electron+Vue项目搭建</a><time datetime="2023-05-14T02:36:59.000Z" title="发表于 2023-05-14 10:36:59">2023-05-14</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/notes/chunkFile/" title="基于SpringBoot与Vue的大文件分片上传与流式下载"><img src= "/img/hexo_loading.gif" data-lazy-src="https://z4a.net/images/2023/04/22/fcc1d07f3136fe8e818242a6534391c8.md.png" onerror="this.onerror=null;this.src='/img/404.jpg'" alt="基于SpringBoot与Vue的大文件分片上传与流式下载"/></a><div class="content"><a class="title" href="/notes/chunkFile/" title="基于SpringBoot与Vue的大文件分片上传与流式下载">基于SpringBoot与Vue的大文件分片上传与流式下载</a><time datetime="2023-05-13T13:57:28.000Z" title="发表于 2023-05-13 21:57:28">2023-05-13</time></div></div></div></div></div></div></main><footer id="footer" style="background: -#484847"><div id="footer-wrap"><div class="copyright">&copy;2023 - 2024 By Lenl</div><div class="framework-info"><span>框架 </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>主题 </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div><div class="footer_custom_text">Hi, welcome to my blog !</div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="阅读模式"><i class="fas fa-book-open"></i></button><button id="darkmode" type="button" title="浅色和深色模式转换"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="单栏和双栏切换"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside_config" type="button" title="设置"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="目录"><i class="fas fa-list-ul"></i></button><a id="to_comment" href="#post-comment" title="直达评论"><i class="fas fa-comments"></i></a><button id="go-up" type="button" title="回到顶部"><span class="scroll-percent"></span><i class="fas fa-arrow-up"></i></button></div></div><div><script src="/js/utils.js"></script><script src="/js/main.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox/fancybox.umd.min.js"></script><script src="https://cdn.jsdelivr.net/npm/instant.page/instantpage.min.js" type="module"></script><script src="https://cdn.jsdelivr.net/npm/vanilla-lazyload/dist/lazyload.iife.min.js"></script><div class="js-pjax"><script>function loadValine () {
  function initValine () {
    const valine = new Valine(Object.assign({
      el: '#vcomment',
      appId: 'dBlOeCLNhBxbSdZO44fRKdBa-gzGzoHsz',
      appKey: 'DCSQoDhPl0UOF7Ix9Imdfd9X',
      avatar: 'monsterid',
      serverURLs: '',
      emojiMaps: "",
      path: window.location.pathname,
      visitor: false
    }, null))
  }

  if (typeof Valine === 'function') initValine() 
  else getScript('https://cdn.jsdelivr.net/npm/valine/dist/Valine.min.js').then(initValine)
}

if ('Valine' === 'Valine' || !false) {
  if (false) btf.loadComment(document.getElementById('vcomment'),loadValine)
  else setTimeout(loadValine, 0)
} else {
  function loadOtherComment () {
    loadValine()
  }
}</script></div><script defer="defer" id="ribbon" src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/canvas-ribbon.min.js" size="150" alpha="0.6" zIndex="-1" mobile="false" data-click="true"></script><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script><div id="local-search"><div class="search-dialog"><nav class="search-nav"><span class="search-dialog-title">搜索</span><span id="loading-status"></span><button class="search-close-button"><i class="fas fa-times"></i></button></nav><div class="is-center" id="loading-database"><i class="fas fa-spinner fa-pulse"></i><span>  数据库加载中</span></div><div class="search-wrap"><div id="local-search-input"><div class="local-search-box"><input class="local-search-box--input" placeholder="搜索文章" type="text"/></div></div><hr/><div class="no-result" id="local-search-results"></div><div id="local-search-stats-wrap"></div></div></div><div id="search-mask"></div><script src="/js/search/local-search.js"></script></div></div></body></html>